Repository navigation
π [v6] contributor-first: a relay in a disabled tier or with no matching work does not cover a lane - #17284
Conversation
β¦ing work does not cover a lane RoleCoverage counted a relay as covering its role when the hub would not hand it work (revoked, or its trust tier in hub.disabled_tiers) and when every ready it sent was answered no_matching_work. Both stay connected, so the governor kept the resident agent's kicks withheld while nobody worked the lane. Seen on hive-wild-mole on 2026-11-08: a relay covered scanner while selection answered no_matching_work every 30 s. Same class as hivecommons#17233 and hivecommons#17268. - Factor revoked / disabled tier into tierAccessGateReason (disabled half: tierDisabled). resumeGateReason keeps its reasons and order; fresh selection's disabled_tiers loop now calls tierDisabled. RoleCoverage reads the tier under c.mu and evaluates with h.mu released. - Record the idle run on the connection: lastIdleReasonSince beside lastIdleReason, set on task_unavailable in the ready and yank paths, no_matching_work and repo_unmintable folded into one run. Cleared on the selection commit, standby dispatch, resume adoption and sendFixTask. - RoleCoverage skips a relay holding no task that has been answered no_matching_work / repo_unmintable for coverageNoWorkWindow (2 min from the first answer; the relay retries every 30 s and the governor still applies contributor_first_grace on top). - Docs: RoleCoverage comment, agent-configuration.md coverage list, governor header, one drifted citation; changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1 Signed-off-by: Danathar <Danathar@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Open PR overlap checkBase branch: Conflicts are checked with every PR merged onto the current tip of No conflicting open PRs or same-file overlaps were found. |
There was a problem hiding this comment.
Read contribute_coverage.go, contribute_select.go, contribute_ws.go, contribute_fixqueue.go and contribute_standby_dispatch.go at the PR head (26ffcec) and checked every writer of the idle-run fields. Looks correct to me; the body's description matches the diff in every particular I checked.
- Idle-run bookkeeping is closed. At the head there are exactly two setters (
contribute_ws.go:2258yank/requeue,:3778handleReady) and four clearers (contribute_select.go:1275selection commit,contribute_ws.go:4195resume adoption,contribute_fixqueue.go:452sendFixTask,contribute_standby_dispatch.go). No remaining rawlastIdleReason =assignment bypasseslastIdleReasonSince. BecauseclearIdleReasonLockedzeroesSince, the run after a task ends restarts from the first post-task empty answer, so a relay that just finished work is not dropped by a stale timestamp. noWorkForWindowLockedis safe against partial state: a cleared reason ("") fails theidleRunKey(...) == no_matching_worktest, so even an unexpected path that only cleared the string could not drop cover.resumeGateReasonordering is preserved. Old: revoked β hub-not-ready β suspended β disabled tier. New (contribute_select.go:377-398):tierAccessGateReasonis computed up front but only an early return forresumeRefusedAccessRevoked; the disabled-tier value is returned after the hub checks.contributorTierRevokedis"revoked"(contribute_profiles.go:533), so the literal-to-constant swap is a no-op.tierDisabledfolds in the olddeps != nil && Config != nilguard.selectTaskPasskeeps the same log line, decision text andtier_disabledreply; only the loop moved.- Lock discipline matches the existing rate check: tier is copied out under
c.mu,tierAccessGateReasonruns afterh.muis released and takes no lock.
One trade-off worth a maintainer's eye, not a defect: once the queue is genuinely empty for the relay, the resident agent will now be kicked after coverageNoWorkWindow + contributor_first_grace and will usually find the same nothing. That is the intended cost per the changelog ("nobody worked the lane" is worse), just noting it is a behaviour change operators will see as extra resident kicks on quiet lanes.
β hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
Summary
A contributor relay that the hub will not hand work to, or has no work for, still counted as covering its contributor-first lane, so the governor kept the resident agent's kicks withheld while nobody worked the lane. This is the same class of bug as #17233 (standby, quota hold) and #17268 (hourly/daily limit). With this change,
RoleCoverageleaves out two more kinds of relay: one that is revoked or whose trust tier is inhub.disabled_tiers, and one that holds no task and has been answeredno_matching_work(orrepo_unmintable) for two minutes. The change is hub-side only. The relay is not touched.What went wrong
tier_disabled, and a resume is refused for a disabled tier or a revoked profile. The socket stays up and heartbeats, soRoleCoveragecounted the relay anyway.readyis answeredno_matching_workis connected and idle. Seen live on 2026-11-08 on hive-wild-mole: the relay covered the scanner lane while selection answeredno_matching_workevery 30 seconds, so the resident scanner was withheld and nobody scanned.Refs #17233, Refs #17268.
What changes
resumeGateReasonintotierAccessGateReason(tier). Its disabled-tier half,tierDisabled(tier), is what fresh selection now calls; selection used to have its own copy of thedisabled_tiersloop.resumeGateReasongives the same reasons in the same order as before: revoked outranks everything, and a disabled tier is reported after the hub checks. Selection's log line, decision text andtier_disabledreply are unchanged, and selection still does not check revoked, which is refused at auth.RoleCoveragecopies the tier out underc.muand callstierAccessGateReasonafterh.muis released, the same way the rate check works.lastIdleReasonalready held the lasttask_unavailablereason. A newlastIdleReasonSincerecords when the current run of that reason began.no_matching_workandrepo_unmintablecount as one run, because the re-scan wrapper can rename one into the other with nothing changing.noteIdleReasonLocked, in thereadyhandler and in the yank/requeue reassignment path. Both already setlastIdleReason.clearIdleReasonLockedon every path that hands the relay work:task_progress;sendFixTask, the one place every fix-pr dispatch goes through.task_unavailable(TASK_UNAVAILABLE_RETRY_MS), so this is about five empty answers in a row. One empty answer between two tasks never reaches it. The window is a time, not an answer count, so a relay that asks less often is judged on how long it has been idle. A longer window is not needed to stop flapping: the governor samples coverage once per evaluation (300 s by default), and a lane that drops out of the report is still withheld forcontributor_first_grace(10 min by default). A relay that finds work again inside the grace keeps the lane without any resident kick.RoleCoveragecomment and the "Contributor-first lanes" section ofsrc/docs/agent-configuration.mdnow list every exclusion in one place.agent-state-inventory.mdis re-pointed.Not changed: governor gate logic, relay code, and how often the relay retries. Other idle reasons (
no_selected_work,capability_mismatch) are left as they were. They are worth a look in a follow-up.Files
src/pkg/dashboard/contribute_coverage.go:RoleCoverage, pluscoverageNoWorkWindowand the idle-run helpers.src/pkg/dashboard/contribute_select.go:tierAccessGateReason/tierDisabled, used byresumeGateReasonandselectTaskPass. Also clears the idle run on assignment.src/pkg/dashboard/contribute_ws.go: thelastIdleReasonSincefield. Records the run in thereadyand yank paths, and clears it on resume adoption.src/pkg/dashboard/contribute_fixqueue.go,contribute_standby_dispatch.go: clear the idle run.src/pkg/governor/contributor_first.go: header comment only.src/docs/agent-configuration.md,src/docs/design/agent-state-inventory.md,changelog.d/fixed-disabled-tier-no-work-relay-not-cover.md.Tests
New tests in
contribute_coverage_test.go. The no-work tests drive realhandleReadyover a socket, so the reasons come from real selection.TestRoleCoverage_DisabledTierDoesNotCoverTestRoleCoverage_RevokedDoesNotCoverTestRoleCoverage_OneNoMatchingWorkAnswerStillCoversTestRoleCoverage_NoMatchingWorkForWindowDoesNotCoverTestRoleCoverage_RepoUnmintableBehavesLikeNoMatchingWorkTestRoleCoverage_FixTaskDispatchEndsNoWorkRunTestRoleCoverage_OtherIdleReasonEndsNoWorkRunTestTierAccessGateReasonAlso ran
go test ./pkg/dashboard/... ./pkg/governor/...,go vet, the docs citation and link checks, and the api-reference citation check.π€ Generated with Claude Code
https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1